#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Henrique Teófilo <henriquetft@gmail.com>

"""
Auxiliar script for Deploy Action.

It Takes ONE vm xml and calls several vbox
commands in order to carry out the deployment.
"""

import math
import os
import subprocess
import sys
import xml.etree.ElementTree as ET


def log(msg):
	f = os.environ.get('ONE_VBOX_LOG', None)
	if not f: return
	try:
		fp = open(f, 'a')
		print >>fp, msg
		print >>fp, "--------------"
		fp.close()
	except:
		pass


class NebulaVM(object):
	def __init__(self, file):
		self.__file = file
		self.__root = ET.parse(file).getroot()

		# attributes
		self.vbox_name = None
		self.cpu = '1'
		self.memory = None
		self.scsi_disks = []
		self.ide_disks = []
		self.nics = []
		self.pae = False
		self.acpi = False
		self.graphics_enabled = False
		self.graphics_address = None
		self.graphics_port = None
		
		# parsing
		self.parse_basic_info()
		self.parse_features()
		self.parse_graphics()
		self.parse_disks()
		self.parse_nics()


	def parse_basic_info(self):
		el_vmid = self.__root.find('VMID')
		vmid = el_vmid.text
		self.vbox_name = "one-%s" % (vmid)
		el_cpu = self.__root.find('CPU')
		try:
			# cpu float number is not allowed in vbox
			self.cpu = str(int(math.ceil(float(el_cpu.text))))
		except:
			pass
		el_memory = self.__root.find('MEMORY')
		self.memory = el_memory.text


	def parse_features(self):
		el_features = self.__root.find('FEATURES')
		if el_features:
			el_acpi = el_features.find('ACPI')
			if el_acpi is not None and el_acpi.text.lower() == 'yes':
				self.acpi = True
			el_pae = el_features.find('PAE')
			if el_pae is not None and el_pae.text.lower() == 'yes':
				self.pae = True


	def parse_graphics(self):
		el_graphics = self.__root.find('GRAPHICS')
		if el_graphics:
			type_el = el_graphics.find('TYPE')
			if type_el.text.lower() == 'vrdp':
				self.graphics_enabled = True
				listen_el = el_graphics.find('LISTEN')
				if listen_el is not None:
					self.graphics_address = listen_el.text
				port_el = el_graphics.find('PORT')
				if port_el is not None:
					self.graphics_port = port_el.text


	def parse_disks(self):
		for el_disk in self.__root.findall('DISK'):
			disk_id_el = el_disk.find('DISK_ID')
			target_el = el_disk.find('TARGET')
			type_el = el_disk.find('TYPE')
			# FIXME: only disks for while...
			if type_el.text == 'DISK':
				self.__add_disks(target_el.text, disk_id_el.text)
			
		el_context = self.__root.find('CONTEXT')
		if el_context is not None:
			# context is the last disk
			context_disk_id = len(self.__root.findall('DISK'))
			el_target = el_context.find('TARGET')
			target = el_target.text
			# an iso must be added as dvddrive with IDE controller
			self.__add_disks(target, context_disk_id, 'dvddrive')


	def parse_nics(self):
		for el_nic in self.__root.findall('NIC'):
			el_bridge = el_nic.find('BRIDGE')
			bridge = el_bridge.text
			el_ip = el_nic.find('IP')
			ip = el_ip.text
			el_mac = el_nic.find('MAC')
			mac = el_mac.text
			self.nics.append({'bridge' : bridge, 'ip' : ip, 'mac' : mac})


	def __add_disks(self, target, disk_id, type='hdd'):
		# NOTE:
		# this device mapping seems to not work properly
		# when arbitrary letters are used in the devices.
		# e.g. specifying disk sdb without having and sda first
		if target.startswith('sd'):
			letter = target.replace('sd', '')
			port = ord(letter) - 97
			self.scsi_disks.append({ 'port' : port, 'disk_id' : disk_id,
			                         'type' : type })
		elif target.startswith('hd'):
			letter = target.replace('hd', '')
			port = ord(letter) - 97
			self.ide_disks.append({ 'port' : port, 'disk_id' : disk_id,
			                        'type' : type })
		else:
			raise Exception, "unknown disk controller: %s" % (target)



################################################################################


def execute(command, ignore_errors=False):
	""" Executes each subcommand to deploy VM.
	Raises exception when exit code is non-zero,
	unless ignore_errors is true """
	log("executing: %s" % (command))
	p = subprocess.Popen([ command ], stdout=subprocess.PIPE, shell=True)
	r = p.communicate()

	(exit_status, stdout, stderr) = (p.returncode, r[0] and r[0] or "",
	                                               r[1] and r[1] or "")

	log("EXIT STATUS: %s" % (exit_status))
	if exit_status != 0:
		log("*** OUTPUT <STDOUT> and <STDERR>: <%s> <%s>" % (stdout, stderr))
	
	if not ignore_errors:
		# NOTE: checking stdout and stderr is a workarround.
		# Sometimes vbox gives exit status zero even on errors.
		if exit_status != 0 or 'error' in stdout.lower() \
		or 'error' in stderr.lower():
			raise Exception, "error exec command: <%s> <%s>" % (stdout, stderr)
	return stdout


def is_vrdp_supported():
	""" Checks whether this vbox version supports VRDP """
	CMD_MANAGE = os.environ.get('CMD_VBOX_MANAGE', 'VBoxManage')
	stdout = execute('%s --help' % (CMD_MANAGE))
	return ('-vrdpaddr' in stdout)


def deploy_vm(vmobject, image_dir):
	# setting app commands...
	CMD_MANAGE = os.environ.get('CMD_VBOX_MANAGE', 'VBoxManage')
	
	execute('%s createvm --name "%s" --register' % (CMD_MANAGE,
	                                                vmobject.vbox_name))
	# features
	execute('%s modifyvm "%s" --acpi %s --pae %s' % \
	           (CMD_MANAGE,
	            vmobject.vbox_name,
	            vmobject.acpi and 'on' or 'off',
	            vmobject.pae and 'on' or 'off'))

	execute('%s modifyvm "%s" --cpus %s' % (CMD_MANAGE, vmobject.vbox_name,
	                                        vmobject.cpu))
	
	execute('%s modifyvm "%s" --memory %s' % \
	                          (CMD_MANAGE, vmobject.vbox_name, vmobject.memory))

	execute('%s modifyvm "%s" --boot1 disk --boot2 none '\
	        '--boot3 none --boot4 none' % (CMD_MANAGE, vmobject.vbox_name))
	
	##### SCSI DISKS
	if vmobject.scsi_disks:
		scsi_name = "SCSI Controller"
		execute('%s storagectl "%s" --name "%s" --add scsi' % \
			                       (CMD_MANAGE, vmobject.vbox_name, scsi_name))
		for x in vmobject.scsi_disks:
			image_file = "%s/disk.%s" % (image_dir, x['disk_id'])
			port = x['port']
			type = x['type']
			
			if type == 'hdd':
				execute("%s internalcommands setvdiuuid '%s'" \
				        % (CMD_MANAGE, image_file), True)
			
			execute('%s storageattach "%s" --storagectl '\
			        '"%s" --port %s --device 0 --type '\
			        '%s --medium %s' % (CMD_MANAGE, vmobject.vbox_name,
			                             scsi_name, port, type, image_file))
	##### IDE DISKS
	if vmobject.ide_disks:
		ide_name = "IDE Controller"
		execute('%s storagectl "%s" --name "%s" --add ide' % \
		                            (CMD_MANAGE, vmobject.vbox_name, ide_name))
		for x in vmobject.ide_disks:
			image_file = "%s/disk.%s" % (image_dir, x['disk_id'])
			port = x['port']
			type = x['type']
			if type == 'hdd':
				execute("%s internalcommands setvdiuuid '%s'" \
				        % (CMD_MANAGE, image_file), True)
			execute('%s storageattach "%s" --storagectl '\
			        '"%s" --port %s --device 0 --type'\
			        ' %s --medium %s' % (CMD_MANAGE, vmobject.vbox_name,
			                             ide_name, port, type, image_file))
	##### NICS
	for id,nic in enumerate(vmobject.nics):
		mac = nic['mac'].replace(":", "")
		bridge = nic['bridge']
		execute('%(cmd)s modifyvm "%(name)s" --nic%(id)d '\
		        'bridged --bridgeadapter%(id)d %(bridge)s '\
		        '--macaddress%(id)d %(mac)s' % \
		        { 'cmd' : CMD_MANAGE, 'id' : id+1,
		          'name' : vmobject.vbox_name,
		          'mac' : mac, 'bridge' : bridge })
	
	# TODO: implement vnc (supported in ose versions)
	
	vrdp_supported = is_vrdp_supported()
	if vmobject.graphics_enabled and vrdp_supported:
		# specifying 'default' or '0' as documentation says
		# is returning error
		VRDP_DEFAULT_PORT = '3389'
		addr = vmobject.graphics_address
		port = vmobject.graphics_port
		if port == "-1": port = None
		execute('%s modifyvm "%s" --vrdp on' % (CMD_MANAGE, vmobject.vbox_name))
		if addr:
			execute('%s modifyvm "%s" --vrdpaddress %s' % (CMD_MANAGE,
			                                               vmobject.vbox_name,
			                                               str(addr)))
		execute('%s modifyvm "%s" --vrdpport %s' % \
		        (CMD_MANAGE, vmobject.vbox_name,
		        (port and str(port) or VRDP_DEFAULT_PORT)))
		
		execute('%s startvm "%s" --type vrdp' % (CMD_MANAGE,
		                                         vmobject.vbox_name))
	elif os.environ.get('ONE_VBOX_HEADLESS_MODE', 'no') == 'yes':
		execute('%s startvm "%s" --type headless' % (CMD_MANAGE,
		                                             vmobject.vbox_name))
	else:
		execute('%s startvm "%s"' % (CMD_MANAGE, vmobject.vbox_name))


# receives domain file name as 1st parameter
# and receives its content from stdin
# e.g. /home/henrique/apps/one2/var//9/deployment.0
if __name__ == "__main__":
	vmobj = None
	exit_code = 0
	if len(sys.argv) < 2:
		raise Exception, "missing argument"
	xml_file = sys.argv[1]
	image_dir = os.path.dirname(xml_file)
	try:
		vmobj = NebulaVM(xml_file)
		deploy_vm(vmobj, image_dir)
	except Exception, e:
		log("PYTHON EXCEPTION: %s" % (str(e)))
		exit_code = 1
	if vmobj:
		print vmobj.vbox_name
	sys.exit(exit_code)
